From cdd2651db054ecd6cd92c9d1ad728fff7cd7ff33 Mon Sep 17 00:00:00 2001 From: Ikey Doherty Date: Sat, 23 Aug 2014 17:38:42 +0100 Subject: [PATCH] Add GtkSidebar GtkSidebar behaves internally much like GtkStackSwitcher, providing a vertical sidebar like widget. It is virtually identical in appearance to the widget currently used in GNOME Tweak Tool. This widget is connected to a GtkStack, and builds its own contents as a GtkListBox subclass, using the "title" child property to provide a consistent navigatable widget. Being a subclass of GtkListBox it benefits immediately from strong keyboard navigation, and minimal changes are required for theming. https://bugzilla.gnome.org/show_bug.cgi?id=735293 Signed-off-by: Ikey Doherty --- demos/gtk-demo/Makefile.am | 1 + demos/gtk-demo/demo.gresource.xml | 1 + demos/gtk-demo/sidebar.c | 77 ++++ docs/reference/gtk/gtk-docs.sgml | 1 + docs/reference/gtk/gtk3-sections.txt | 20 ++ docs/reference/gtk/gtk3.types.in | 1 + docs/reference/gtk/images/sidebar.png | Bin 0 -> 8465 bytes docs/reference/gtk/visual_index.xml | 3 + gtk/Makefile.am | 2 + gtk/gtk.h | 1 + gtk/gtksidebar.c | 499 ++++++++++++++++++++++++++ gtk/gtksidebar.h | 74 ++++ gtk/makefile.msc.in | 2 + tests/teststack.c | 13 +- 14 files changed, 693 insertions(+), 2 deletions(-) create mode 100644 demos/gtk-demo/sidebar.c create mode 100644 docs/reference/gtk/images/sidebar.png create mode 100644 gtk/gtksidebar.c create mode 100644 gtk/gtksidebar.h diff --git a/demos/gtk-demo/Makefile.am b/demos/gtk-demo/Makefile.am index 0ef288f78c..6b0b8da6a6 100644 --- a/demos/gtk-demo/Makefile.am +++ b/demos/gtk-demo/Makefile.am @@ -48,6 +48,7 @@ demos = \ rotated_text.c \ search_entry.c \ search_entry2.c \ + sidebar.c \ sizegroup.c \ spinner.c \ stack.c \ diff --git a/demos/gtk-demo/demo.gresource.xml b/demos/gtk-demo/demo.gresource.xml index 4f90eb4d1e..e9c628dbf3 100644 --- a/demos/gtk-demo/demo.gresource.xml +++ b/demos/gtk-demo/demo.gresource.xml @@ -118,6 +118,7 @@ search_entry.c search_entry2.c sizegroup.c + sidebar.c stack.c spinner.c textview.c diff --git a/demos/gtk-demo/sidebar.c b/demos/gtk-demo/sidebar.c new file mode 100644 index 0000000000..2d373f4a89 --- /dev/null +++ b/demos/gtk-demo/sidebar.c @@ -0,0 +1,77 @@ +/* Sidebar + * + * GtkSidebar provides an automatic sidebar widget to control navigation + * of a GtkStack object. This widget automatically updates it content + * based on what is presently available in the GtkStack object, and + * using the "title" child property to set the display labels. + */ + +#include +#include + +static GtkWidget *window = NULL; + +GtkWidget * +do_sidebar (GtkWidget *do_widget) +{ + GtkWidget *sidebar; + GtkWidget *stack; + GtkWidget *box; + GtkWidget *widget; + GtkWidget *header; + const gchar* pages[] = { "Welcome to GTK+", "GtkSidebar Widget", "Automatic navigation", "Consistent appearance", NULL }; + const gchar *c = NULL; + guint i; + + if (!window) + { + window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + gtk_window_set_resizable (GTK_WINDOW (window), TRUE); + gtk_widget_set_size_request (window, 500, 350); + + header = gtk_header_bar_new (); + gtk_header_bar_set_show_close_button (GTK_HEADER_BAR(header), TRUE); + gtk_window_set_titlebar (GTK_WINDOW(window), header); + gtk_window_set_title (GTK_WINDOW(window), "Sidebar demo"); + + g_signal_connect (window, "destroy", + G_CALLBACK (gtk_widget_destroyed), &window); + + box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + sidebar = gtk_sidebar_new (); + gtk_box_pack_start (GTK_BOX (box), sidebar, FALSE, FALSE, 0); + + stack = gtk_stack_new (); + gtk_stack_set_transition_type (GTK_STACK (stack), GTK_STACK_TRANSITION_TYPE_SLIDE_UP_DOWN); + gtk_sidebar_set_stack (GTK_SIDEBAR (sidebar), GTK_STACK (stack)); + + /* Separator between sidebar and stack */ + widget = gtk_separator_new (GTK_ORIENTATION_VERTICAL); + gtk_box_pack_start (GTK_BOX(box), widget, FALSE, FALSE, 0); + + gtk_box_pack_start (GTK_BOX (box), stack, TRUE, TRUE, 0); + + for (i=0; (c = *(pages+i)) != NULL; i++ ) + { + if (i == 0) + { + widget = gtk_image_new_from_icon_name ("help-about", GTK_ICON_SIZE_INVALID); + gtk_image_set_pixel_size (GTK_IMAGE (widget), 256); + } else + { + widget = gtk_label_new (c); + } + gtk_stack_add_named (GTK_STACK (stack), widget, c); + gtk_container_child_set (GTK_CONTAINER (stack), widget, "title", c, NULL); + } + + gtk_container_add (GTK_CONTAINER (window), box); + } + + if (!gtk_widget_get_visible (window)) + gtk_widget_show_all (window); + else + gtk_widget_destroy (window); + + return window; +} diff --git a/docs/reference/gtk/gtk-docs.sgml b/docs/reference/gtk/gtk-docs.sgml index 2e00bae4bf..e3956d6608 100644 --- a/docs/reference/gtk/gtk-docs.sgml +++ b/docs/reference/gtk/gtk-docs.sgml @@ -76,6 +76,7 @@ + diff --git a/docs/reference/gtk/gtk3-sections.txt b/docs/reference/gtk/gtk3-sections.txt index 41fb440770..9058cd7172 100644 --- a/docs/reference/gtk/gtk3-sections.txt +++ b/docs/reference/gtk/gtk3-sections.txt @@ -8157,3 +8157,23 @@ GTK_GESTURE_ZOOM_GET_CLASS gtk_gesture_zoom_get_type + + +
+gtksidebar +GtkSidebar +GtkSidebarClass +gtk_sidebar_new + + +GTK_TYPE_SIDEBAR +GTK_SIDEBAR +GTK_SIDEBAR_CLASS +GTK_IS_SIDEBAR +GTK_IS_SIDEBAR_CLASS +GTK_SIDEBAR_GET_CLASS + + +GtkSidebarPrivate +gtk_sidebar_get_type +
diff --git a/docs/reference/gtk/gtk3.types.in b/docs/reference/gtk/gtk3.types.in index 87c6d8fde8..be467fffb3 100644 --- a/docs/reference/gtk/gtk3.types.in +++ b/docs/reference/gtk/gtk3.types.in @@ -170,6 +170,7 @@ gtk_separator_get_type gtk_separator_menu_item_get_type gtk_separator_tool_item_get_type gtk_settings_get_type +gtk_sidebar_get_type gtk_size_group_get_type @ENABLE_ON_X11@gtk_socket_get_type gtk_spin_button_get_type diff --git a/docs/reference/gtk/images/sidebar.png b/docs/reference/gtk/images/sidebar.png new file mode 100644 index 0000000000000000000000000000000000000000..d7be78fc08eea1e3809d99bd74408d5a23069e7f GIT binary patch literal 8465 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 + + + diff --git a/gtk/Makefile.am b/gtk/Makefile.am index f1de3f73da..fcae22bb8c 100644 --- a/gtk/Makefile.am +++ b/gtk/Makefile.am @@ -431,6 +431,7 @@ gtk_public_h_sources = \ gtkseparatortoolitem.h \ gtksettings.h \ gtkshow.h \ + gtksidebar.h \ gtksizegroup.h \ gtksizerequest.h \ gtksocket.h \ @@ -1027,6 +1028,7 @@ gtk_base_c_sources = \ gtksizerequest.c \ gtksizerequestcache.c \ gtkshow.c \ + gtksidebar.c \ gtkspinbutton.c \ gtkspinner.c \ gtkstack.c \ diff --git a/gtk/gtk.h b/gtk/gtk.h index b027dc6c7e..330f14a315 100644 --- a/gtk/gtk.h +++ b/gtk/gtk.h @@ -180,6 +180,7 @@ #include #include #include +#include #include #include #include diff --git a/gtk/gtksidebar.c b/gtk/gtksidebar.c new file mode 100644 index 0000000000..aafdfe9172 --- /dev/null +++ b/gtk/gtksidebar.c @@ -0,0 +1,499 @@ +/* + * Copyright (c) 2014 Intel Corporation + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + * Author: + * Ikey Doherty + */ + +#include "config.h" + +#include "gtksidebar.h" +#include "gtklistbox.h" +#include "gtkseparator.h" +#include "gtkstylecontext.h" +#include "gtklabel.h" +#include "gtkprivate.h" +#include "gtkintl.h" + +/** + * SECTION:gtk-sidebar + * @short_description: An automatic sidebar widget + * @title: GtkSidebar + * + * A GtkSidebarWindow enables you to quickly and easily provide a consistent + * "sidebar" object for your user interface. + * + * In order to use a GtkSidebar, you simply use a GtkStack to organise + * your UI flow, and add the sidebar to your sidebar area. You can use + * gtk_sidebar_set_stack() to connect the #GtkSidebar to the #GtkStack. + * + * Since: 3.16 + */ + +struct _GtkSidebarPrivate +{ + GtkStack *stack; + GHashTable *rows; + gboolean in_child_changed; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (GtkSidebar, gtk_sidebar, GTK_TYPE_LIST_BOX) + +enum +{ + PROP_0, + PROP_STACK, + N_PROPERTIES +}; +static GParamSpec *obj_properties[N_PROPERTIES] = { NULL, }; + +static void +gtk_sidebar_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (prop_id) + { + case PROP_STACK: + gtk_sidebar_set_stack (GTK_SIDEBAR (object), g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gtk_sidebar_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtkSidebarPrivate *priv = gtk_sidebar_get_instance_private (GTK_SIDEBAR (object)); + + switch (prop_id) + { + case PROP_STACK: + g_value_set_object (value, priv->stack); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +update_header (GtkListBoxRow *row, + GtkListBoxRow *before, + gpointer userdata) +{ + GtkWidget *ret = NULL; + + if (before && !gtk_list_box_row_get_header (row)) + { + ret = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); + gtk_list_box_row_set_header (row, ret); + } +} + +static gint +sort_list (GtkListBoxRow *row1, + GtkListBoxRow *row2, + gpointer userdata) +{ + GtkSidebar *sidebar = GTK_SIDEBAR (userdata); + GtkSidebarPrivate *priv = gtk_sidebar_get_instance_private (sidebar); + GtkWidget *item; + GtkWidget *widget; + gint left = 0; gint right = 0; + + + if (row1) + { + item = gtk_bin_get_child (GTK_BIN (row1)); + widget = g_object_get_data (G_OBJECT (item), "stack-child"); + gtk_container_child_get (GTK_CONTAINER (priv->stack), widget, + "position", &left, + NULL); + } + + if (row2) + { + item = gtk_bin_get_child (GTK_BIN (row2)); + widget = g_object_get_data (G_OBJECT (item), "stack-child"); + gtk_container_child_get (GTK_CONTAINER (priv->stack), widget, + "position", &right, + NULL); + } + + if (left < right) + return -1; + + if (left == right) + return 0; + + return 1; +} + +static void +gtk_sidebar_row_selected (GtkListBox *box, + GtkListBoxRow *row) +{ + GtkSidebar *sidebar = GTK_SIDEBAR (box); + GtkSidebarPrivate *priv = gtk_sidebar_get_instance_private (sidebar); + GtkWidget *item; + GtkWidget *widget; + + if (priv->in_child_changed) + return; + + if (!row) + return; + + item = gtk_bin_get_child (GTK_BIN (row)); + widget = g_object_get_data (G_OBJECT (item), "stack-child"); + gtk_stack_set_visible_child (priv->stack, widget); +} + +static void +gtk_sidebar_init (GtkSidebar *sidebar) +{ + GtkStyleContext *style; + GtkSidebarPrivate *priv; + + priv = gtk_sidebar_get_instance_private (sidebar); + + gtk_list_box_set_header_func (GTK_LIST_BOX (sidebar), update_header, sidebar, NULL); + gtk_list_box_set_sort_func (GTK_LIST_BOX (sidebar), sort_list, sidebar, NULL); + + style = gtk_widget_get_style_context (GTK_WIDGET (sidebar)); + gtk_style_context_add_class (style, "sidebar"); + + /* Store this for later use */ + priv->rows = g_hash_table_new (NULL, NULL); +} + +static void +update_row (GtkSidebar *sidebar, + GtkWidget *widget, + GtkWidget *row) +{ + GtkSidebarPrivate *priv = gtk_sidebar_get_instance_private (sidebar); + GtkWidget *item; + gchar *title; + gboolean needs_attention; + GtkStyleContext *context; + + gtk_container_child_get (GTK_CONTAINER (priv->stack), widget, + "title", &title, + "needs-attention", &needs_attention, + NULL); + + item = gtk_bin_get_child (GTK_BIN (row)); + gtk_label_set_text (GTK_LABEL (item), title); + + gtk_widget_set_visible (row, gtk_widget_get_visible (widget) && title != NULL); + + context = gtk_widget_get_style_context (row); + if (needs_attention) + gtk_style_context_add_class (context, GTK_STYLE_CLASS_NEEDS_ATTENTION); + else + gtk_style_context_remove_class (context, GTK_STYLE_CLASS_NEEDS_ATTENTION); + + g_free (title); +} + +static void +on_position_updated (GtkWidget *widget, + GParamSpec *pspec, + GtkSidebar *sidebar) +{ + gtk_list_box_invalidate_sort (GTK_LIST_BOX (sidebar)); +} + +static void +on_child_updated (GtkWidget *widget, + GParamSpec *pspec, + GtkSidebar *sidebar) +{ + GtkSidebarPrivate *priv = gtk_sidebar_get_instance_private (sidebar); + GtkWidget *row; + + row = g_hash_table_lookup (priv->rows, widget); + update_row (sidebar, widget, row); +} + +static void +add_child (GtkWidget *widget, + GtkSidebar *sidebar) +{ + GtkSidebarPrivate *priv = gtk_sidebar_get_instance_private (sidebar); + GtkStyleContext *style; + GtkWidget *item; + GtkWidget *row; + + /* Check we don't actually already know about this widget */ + if (g_hash_table_lookup (priv->rows, widget)) + return; + + /* Make a pretty item when we add kids */ + item = gtk_label_new (""); + gtk_widget_set_halign (item, GTK_ALIGN_START); + gtk_widget_set_valign (item, GTK_ALIGN_CENTER); + row = gtk_list_box_row_new (); + gtk_container_add (GTK_CONTAINER (row), item); + gtk_widget_show (item); + + update_row (sidebar, widget, row); + + /* Fix up styling */ + style = gtk_widget_get_style_context (row); + gtk_style_context_add_class (style, "sidebar-item"); + + /* Hook up for events */ + g_signal_connect (widget, "child-notify::title", + G_CALLBACK (on_child_updated), sidebar); + g_signal_connect (widget, "child-notify::needs-attention", + G_CALLBACK (on_child_updated), sidebar); + g_signal_connect (widget, "notify::visible", + G_CALLBACK (on_child_updated), sidebar); + g_signal_connect (widget, "child-notify::position", + G_CALLBACK (on_position_updated), sidebar); + + g_object_set_data (G_OBJECT (item), "stack-child", widget); + g_hash_table_insert (priv->rows, widget, row); + gtk_container_add (GTK_CONTAINER (sidebar), row); +} + +static void +remove_child (GtkWidget *widget, + GtkSidebar *sidebar) +{ + GtkSidebarPrivate *priv = gtk_sidebar_get_instance_private (sidebar); + GtkWidget *row; + + row = g_hash_table_lookup (priv->rows, widget); + if (!row) + return; + + g_signal_handlers_disconnect_by_func (widget, on_child_updated, sidebar); + g_signal_handlers_disconnect_by_func (widget, on_position_updated, sidebar); + + gtk_container_remove (GTK_CONTAINER (sidebar), row); + g_hash_table_remove (priv->rows, widget); +} + +static void +populate_sidebar (GtkSidebar *sidebar) +{ + GtkSidebarPrivate *priv = gtk_sidebar_get_instance_private (sidebar); + + gtk_container_foreach (GTK_CONTAINER (priv->stack), (GtkCallback)add_child, sidebar); +} + +static void +clear_sidebar (GtkSidebar *sidebar) +{ + GtkSidebarPrivate *priv = gtk_sidebar_get_instance_private (sidebar); + + gtk_container_foreach (GTK_CONTAINER (priv->stack), (GtkCallback)remove_child, sidebar); +} + +static void +on_child_changed (GtkWidget *widget, + GParamSpec *pspec, + GtkSidebar *sidebar) +{ + GtkSidebarPrivate *priv = gtk_sidebar_get_instance_private (sidebar); + GtkWidget *child; + GtkWidget *row; + + child = gtk_stack_get_visible_child (GTK_STACK (widget)); + row = g_hash_table_lookup (priv->rows, child); + if (row != NULL) + { + priv->in_child_changed = TRUE; + gtk_list_box_select_row (GTK_LIST_BOX (sidebar), GTK_LIST_BOX_ROW (row)); + priv->in_child_changed = FALSE; + } +} + +static void +on_stack_child_added (GtkContainer *container, + GtkWidget *widget, + GtkSidebar *sidebar) +{ + add_child (widget, sidebar); +} + +static void +on_stack_child_removed (GtkContainer *container, + GtkWidget *widget, + GtkSidebar *sidebar) +{ + remove_child (widget, sidebar); +} + +static void +disconnect_stack_signals (GtkSidebar *sidebar) +{ + GtkSidebarPrivate *priv = gtk_sidebar_get_instance_private (sidebar); + + g_signal_handlers_disconnect_by_func (priv->stack, on_stack_child_added, sidebar); + g_signal_handlers_disconnect_by_func (priv->stack, on_stack_child_removed, sidebar); + g_signal_handlers_disconnect_by_func (priv->stack, on_child_changed, sidebar); + g_signal_handlers_disconnect_by_func (priv->stack, disconnect_stack_signals, sidebar); +} + +static void +connect_stack_signals (GtkSidebar *sidebar) +{ + GtkSidebarPrivate *priv = gtk_sidebar_get_instance_private (sidebar); + + g_signal_connect_after (priv->stack, "add", + G_CALLBACK (on_stack_child_added), sidebar); + g_signal_connect_after (priv->stack, "remove", + G_CALLBACK (on_stack_child_removed), sidebar); + g_signal_connect (priv->stack, "notify::visible-child", + G_CALLBACK (on_child_changed), sidebar); + g_signal_connect_swapped (priv->stack, "destroy", + G_CALLBACK (disconnect_stack_signals), sidebar); +} + +static void +gtk_sidebar_dispose (GObject *object) +{ + GtkSidebar *sidebar = GTK_SIDEBAR (object); + + gtk_sidebar_set_stack (sidebar, NULL); + + G_OBJECT_CLASS (gtk_sidebar_parent_class)->dispose (object); +} + +static void +gtk_sidebar_finalize (GObject *object) +{ + GtkSidebar *sidebar = GTK_SIDEBAR (object); + GtkSidebarPrivate *priv = gtk_sidebar_get_instance_private (sidebar); + + g_hash_table_destroy (priv->rows); + + G_OBJECT_CLASS (gtk_sidebar_parent_class)->finalize (object); +} + +static void +gtk_sidebar_class_init (GtkSidebarClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkListBoxClass *list_box_class = GTK_LIST_BOX_CLASS (klass); + + object_class->dispose = gtk_sidebar_dispose; + object_class->finalize = gtk_sidebar_finalize; + object_class->set_property = gtk_sidebar_set_property; + object_class->get_property = gtk_sidebar_get_property; + + list_box_class->row_selected = gtk_sidebar_row_selected; + + obj_properties[PROP_STACK] = + g_param_spec_pointer ("stack", P_("Stack"), + P_("Associated stack for this GtkSidebar"), + G_PARAM_READWRITE); + + g_object_class_install_properties (object_class, N_PROPERTIES, obj_properties); +} + +/** + * gtk_sidebar_new: + * + * Creates a new sidebar. + * + * Returns: the new #GtkSidebar + * + * Since: 3.16 + */ +GtkWidget * +gtk_sidebar_new (void) +{ + return GTK_WIDGET (g_object_new (GTK_TYPE_SIDEBAR, NULL)); +} + +/** + * gtk_sidebar_set_stack: + * @sidebar: a #GtkSidebar + * @stack: a #GtkStack + * + * Set the #GtkStack associated with this #GtkSidebar. + * + * The sidebar widget will automatically update according to the order + * (packing) and items within the given #GtkStack. + * + * Since: 3.16 + */ +void +gtk_sidebar_set_stack (GtkSidebar *sidebar, + GtkStack *stack) +{ + GtkSidebarPrivate *priv; + + g_return_if_fail (GTK_IS_SIDEBAR (sidebar)); + g_return_if_fail (GTK_IS_STACK (stack) || stack == NULL); + + priv = gtk_sidebar_get_instance_private (sidebar); + + if (priv->stack == stack) + return; + + if (priv->stack) + { + disconnect_stack_signals (sidebar); + clear_sidebar (sidebar); + g_clear_object (&priv->stack); + } + if (stack) + { + priv->stack = g_object_ref (stack); + populate_sidebar (sidebar); + connect_stack_signals (sidebar); + } + + gtk_widget_queue_resize (GTK_WIDGET (sidebar)); + + g_object_notify (G_OBJECT (sidebar), "stack"); +} + +/** + * gtk_sidebar_get_stack: + * @sidebar: a #GtkSidebar + * + * Returns: (transfer full): the associated #GtkStack + * + * Since: 3.16 + */ +GtkStack * +gtk_sidebar_get_stack (GtkSidebar *sidebar) +{ + GtkSidebarPrivate *priv; + + g_return_if_fail (GTK_IS_SIDEBAR (sidebar)); + + priv = gtk_sidebar_get_instance_private (sidebar); + + return GTK_STACK (priv->stack); +} diff --git a/gtk/gtksidebar.h b/gtk/gtksidebar.h new file mode 100644 index 0000000000..2f076a6c48 --- /dev/null +++ b/gtk/gtksidebar.h @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2014 Intel Corporation + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + * Author: + * Ikey Doherty + */ + +#ifndef __GTK_SIDEBAR_H__ +#define __GTK_SIDEBAR_H__ + +#if !defined (__GTK_H_INSIDE__) && !defined (GTK_COMPILATION) +#error "Only can be included directly." +#endif + +#include +#include +#include + +G_BEGIN_DECLS + +#define GTK_TYPE_SIDEBAR (gtk_sidebar_get_type ()) +#define GTK_SIDEBAR(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_SIDEBAR, GtkSidebar)) +#define GTK_IS_SIDEBAR(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_SIDEBAR)) +#define GTK_SIDEBAR_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_SIDEBAR, GtkSidebarClass)) +#define GTK_IS_SIDEBAR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_SIDEBAR)) +#define GTK_SIDEBAR_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_SIDEBAR, GtkSidebarClass)) + +typedef struct _GtkSidebar GtkSidebar; +typedef struct _GtkSidebarPrivate GtkSidebarPrivate; +typedef struct _GtkSidebarClass GtkSidebarClass; + +struct _GtkSidebar +{ + GtkListBox parent; +}; + +struct _GtkSidebarClass +{ + GtkListBoxClass parent_class; + + /* Padding for future expansion */ + void (*_gtk_reserved1) (void); + void (*_gtk_reserved2) (void); + void (*_gtk_reserved3) (void); + void (*_gtk_reserved4) (void); +}; + +GDK_AVAILABLE_IN_3_16 +GType gtk_sidebar_get_type (void) G_GNUC_CONST; +GDK_AVAILABLE_IN_3_16 +GtkWidget * gtk_sidebar_new (void); +GDK_AVAILABLE_IN_3_16 +void gtk_sidebar_set_stack (GtkSidebar *sidebar, + GtkStack *stack); +GDK_AVAILABLE_IN_3_16 +GtkStack * gtk_sidebar_get_stack (GtkSidebar *sidebar); + +G_END_DECLS + +#endif /* __GTK_SIDEBAR_H__ */ diff --git a/gtk/makefile.msc.in b/gtk/makefile.msc.in index 619b87de92..3ee1baaaa2 100644 --- a/gtk/makefile.msc.in +++ b/gtk/makefile.msc.in @@ -283,6 +283,7 @@ gtk_OBJECTS = \ gtkseparatortoolitem.obj \ gtksettings.obj \ gtkshow.obj \ + gtksidebar.obj \ gtksizegroup.obj \ gtksizerequest.obj \ gtkspinbutton.obj \ @@ -475,6 +476,7 @@ gtk_public_h_sources = \ gtkseparatortoolitem.h \ gtksettings.h \ gtkshow.h \ + gtksidebar.h \ gtksizegroup.h \ gtksizerequest.h \ gtksocket.h \ diff --git a/tests/teststack.c b/tests/teststack.c index 35c7abcc0a..2c9e5fa030 100644 --- a/tests/teststack.c +++ b/tests/teststack.c @@ -2,6 +2,7 @@ GtkWidget *stack; GtkWidget *switcher; +GtkWidget *sidebar; GtkWidget *w1; static void @@ -99,7 +100,7 @@ gint main (gint argc, gchar ** argv) { - GtkWidget *window, *box, *button, *hbox, *combo; + GtkWidget *window, *box, *button, *hbox, *combo, *layout; GtkWidget *w2, *w3; GtkListStore* store; GtkWidget *tree_view; @@ -127,7 +128,15 @@ main (gint argc, gtk_stack_set_transition_duration (GTK_STACK (stack), 1500); gtk_widget_set_halign (stack, GTK_ALIGN_START); - gtk_container_add (GTK_CONTAINER (box), stack); + + /* Add sidebar before stack */ + sidebar = gtk_sidebar_new (); + gtk_sidebar_set_stack (GTK_SIDEBAR (sidebar), GTK_STACK (stack)); + layout = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_pack_start (GTK_BOX (layout), sidebar, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (layout), stack, TRUE, TRUE, 0); + + gtk_container_add (GTK_CONTAINER (box), layout); gtk_stack_switcher_set_stack (GTK_STACK_SWITCHER (switcher), GTK_STACK (stack)); -- 2.30.2